package parser;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.LinkedHashMap;
import java.util.Map;

import model.Empty;
import model.NonTerminal;
import model.Rule;
import model.Term;
import model.Terminal;

/**
 * De parser die de BNF grammatica over input heen haalt
 * @author Dave en Timo
 *
 */
public class Parser {
	private Scanner scanner;
	private Token currentToken;
	private NonTerminal startNonTerminal;
	
	private String currentKey;
	private Map<String, Rule> rules;
	
	public Parser(String filename) {		
		FileInputStream file;
		try {
			file = new FileInputStream(filename);
			scanner = new Scanner(new BufferedReader(new InputStreamReader(file)));
			
			startNonTerminal = null;
			rules = new LinkedHashMap<String, Rule>();
			
			acceptIt();
			parseBnf();
			accept(TokenKind.EOF);
			
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		}
	}
	
	// <bnf> ::= <rule> <restRules>
	public void parseBnf() {
		parseRule();
		parseRestRules();
	}

	// <rule> ::= NONTERMINAL ISDEFINEDAS <alternatives> EOL
	private void parseRule() {
		if(currentToken.getTokenKind() == TokenKind.NONTERMINAL) {
			currentKey = currentToken.getText();
			Rule rule = (Rule) rules.get(currentKey);
			if(rule == null) { // Er is al een regel met deze key
				rule = new Rule(new NonTerminal(currentToken.getText()));
				rules.put(currentKey, rule);
			}
			
			if(startNonTerminal == null) {
				startNonTerminal = new NonTerminal(currentToken.getText());
			}
			
			rule.newProductionLine();
			
			acceptIt();
			accept(TokenKind.ISDEFINEDAS);
			parseAlternatives();
			accept(TokenKind.EOL);
		}
	}
	
	// <restRules> ::= <bnf> | ε
	private void parseRestRules() {
		if(currentToken.getTokenKind() == TokenKind.NONTERMINAL || currentToken.getTokenKind() == TokenKind.TERMINAL) {
			parseBnf();
		}
	}
	
	// <alternatives> ::= <oneAlternative> <otherAlternatives>
	private void parseAlternatives() {
		parseOneAlternative();
		parseOtherAlternatives();
	}
	
	// <oneAlternative> ::= EMPTY | <term> <restAlternative>
	private void parseOneAlternative() {
		if(currentToken.getTokenKind() == TokenKind.NONTERMINAL || currentToken.getTokenKind() == TokenKind.TERMINAL) {
			parseTerm();
			parseRestAlternative();
		} else if(currentToken.getTokenKind() == TokenKind.EMPTY) {
			rules.get(currentKey).getLastProductionLine().addTerm(new Empty(currentToken.getText()));
			acceptIt();
		}
	}
	
	// <otherAlternatives> ::= ε | OR <alternatives>
	private void parseOtherAlternatives() {
		if(currentToken.getTokenKind() == TokenKind.OR) {
			acceptIt();
			rules.get(currentKey).newProductionLine();
			parseAlternatives();
		}
	}

	// <restAlternative> ::= <term> <restAlternative> | ε
	private void parseRestAlternative() {
		if(currentToken.getTokenKind() == TokenKind.NONTERMINAL || currentToken.getTokenKind() == TokenKind.TERMINAL) {
			parseTerm();
			parseRestAlternative();
		}
	}
	
	// <term> ::= TERMINAL | NONTERMINAL
	private void parseTerm() {
		if(currentToken.getTokenKind() == TokenKind.NONTERMINAL || currentToken.getTokenKind() == TokenKind.TERMINAL) {

			Term term = null;
			if(currentToken.getTokenKind() == TokenKind.NONTERMINAL) {
				term = new NonTerminal(currentToken.getText());
			} else if(currentToken.getTokenKind() == TokenKind.TERMINAL) {
				term = new Terminal(currentToken.getText());
			}
			
			rules.get(currentKey).getLastProductionLine().addTerm(term);
			acceptIt();
		} else {
			parseError(currentToken +" not expected");
		}
	}
	
	public Map<String, Rule> getRules() {
		return rules;
	}
	
	public NonTerminal getStartNonTerminal() {
		return startNonTerminal;
	}
	
	/**
	 * Accept when expected / else error
	 * @param expectedTokenKind
	 */
	private void accept(TokenKind expectedTokenKind) {
		if(currentToken.getTokenKind() == expectedTokenKind) {
			acceptIt();
		} else {
			parseError("Kind=" + expectedTokenKind + " expected");
		}
	}
	
	/**
	 * Go to next token
	 */
	private void acceptIt() {
		try {
			currentToken = scanner.nextToken();
		} catch (IOException e) {
			e.printStackTrace();
			throw new RuntimeException();
		}
	}
	
	private void parseError(String message) {
		System.err.println();
		System.err.println("parse error " + message);
		System.err.print("lookahead '" + currentToken.getText() + "' ");
		System.err.print("(kind=" + currentToken.getTokenKind() + ") ");
		System.err.print("at " + currentToken.getLine() + "/"+ currentToken.getColumn());
		System.err.println();
		throw new RuntimeException();
	}
}
